using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Oops.Xna.Framework.Input;

namespace Oops.Xna.Framework.Viewpoint.Cameras
{
	/// <summary>
	/// Represents a camera with arc ball functionality.
	/// </summary>
	public class ArcBallCamera : Camera
	{
		private IInputService inputService;

		/// <summary>
		/// Gets or sets, the minimum arc, in radians, of the <see cref="ArcBallCamera"/>.
		/// </summary>
		public float MinimumArc = -MathHelper.PiOver2;

		/// <summary>
		/// Gets or sets the maximum arc, in radians, of the <see cref="ArcBallCamera"/>.
		/// </summary>
		public float MaximumArc = 0.0f;

		/// <summary>
		/// Gets or sets the minimum distance from the <see cref="ArcBallCamera.Target"/>.
		/// </summary>
		public float MinimmDistance = 50.0f;

		/// <summary>
		/// Gets or sets the maximum distance from the <see cref="ArcBallCamera.Target"/>.
		/// </summary>
		public float MaximumDistance = 200.0f;

		/// <summary>
		/// Gets or sets the position that the <see cref="ArcBallCamera"/> always looks at.
		/// </summary>
		public Vector3 Target;

		/// <summary>
		/// Initializes a new instance of the <see cref="ArcBallCamera"/> class.
		/// </summary>
		/// <param name="game">The associated <see cref="Microsoft.Xna.Framework.Game"/>.</param>
		/// <param name="name">The name of the <see cref="ArcBallCamera"/></param>
		public ArcBallCamera(Game game, string name) : base(game, name)
		{ 
		}

		/// <summary>
		/// Gets the <see cref="IInputService"/> in context.
		/// </summary>
		protected IInputService InputService
		{
			get { return this.inputService; }
		}

		#region IGameComponent Members

		public override void Initialize()
		{
			// Get the input service.
			this.inputService = this.Game.GetService<IInputService>();
			
			// Call inherited method.
			base.Initialize();
		}

		#endregion

		#region IUpdateable Members

		public override void Update(GameTime gameTime)
		{
			// Get elapsed time.
			float elapsed = (float)gameTime.ElapsedGameTime.TotalMilliseconds;

			// Get the yaw, pitch and roll of the camera's orientation.
			float yaw, pitch, roll;
#if REFERENCE
			MatrixExtension.Decompose(ref this.Orientation, out yaw, out pitch, out roll);
#else
			this.Orientation.Decompose(out yaw, out pitch, out roll);
#endif

			// Get the distance from the target.
#if INLINE
			float x = this.Position.X - this.Target.X;
			float y = this.Position.Y - this.Target.Y;
			float z = this.Position.Z - this.Target.Z;
			float distance = (float)Math.Sqrt(x * x + y * y + z * z);
#elif REFERENCE
			float distance;
			Vector3.Distance(ref this.Position, ref this.Target, out distance);
#else
			float distance = Vector3.Distance(this.Position, this.Target);
#endif
			// The left thumb stick y-axis and W/S keys control pitch.
			pitch -= this.InputService.GetGamePadLeftThumbStick(0).Y * elapsed * 0.005f;
			if (this.inputService.IsKeyDown(Keys.W))
			{
				pitch -= elapsed * 0.005f;
			}
			if (this.inputService.IsKeyDown(Keys.S))
			{
				pitch += elapsed * 0.005f;
			}
			pitch = MathHelper.Clamp(pitch, this.MinimumArc, this.MaximumArc);

			// The left thumb stick x-axis and A/D keys control yaw.
			yaw += this.InputService.GetGamePadLeftThumbStick(0).X * elapsed * 0.005f;
			if (this.inputService.IsKeyDown(Keys.A))
			{
				yaw -= elapsed * 0.005f;
			}
			if (this.inputService.IsKeyDown(Keys.D))
			{
				yaw += elapsed * 0.005f;
			}

			// The triggers and X/C keys control the distance.
			distance  += (this.InputService.GetGamePadRightTrigger(0) - this.InputService.GetGamePadLeftTrigger(0)) * elapsed * 0.5f;
			if (this.inputService.IsKeyDown(Keys.X))
			{
				distance -= elapsed * 0.5f;
			}
			if (this.inputService.IsKeyDown(Keys.C))
			{
				distance += elapsed * 0.5f;
			}
			distance = MathHelper.Clamp(distance, this.MinimmDistance, this.MaximumDistance);
			
			// Determine the position and orienatation of the camera.
#if REFERENCE
			Matrix.CreateFromYawPitchRoll(yaw, pitch, roll, out this.Orientation);
#else
			this.Orientation = Matrix.CreateFromYawPitchRoll(yaw, pitch, roll);
#endif
#if INLINE
			this.Position.X = this.Target.X + (this.Orientation.M31 * distance);
			this.Position.Y = this.Target.Y + (this.Orientation.M32 * distance);
			this.Position.Z = this.Target.Z + (this.Orientation.M33 * distance);
#elif REFERENCE
			Vector3 temp = this.Orientation.Backward;
			Vector3.Multiply(ref temp, distance, out temp);
			Vector3.Add(ref this.Target, ref temp, out this.Position);
#else
			this.Position = this.Target + (this.Orientation.Backward * distance);
#endif
			// Call inherited method.
			base.Update(gameTime);
		}

		#endregion
	}
}
